package no.ffi.gismoidm.servicestubs;

import java.io.IOException;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.io.PrintWriter;
import java.io.Serializable;
import java.security.PublicKey;
import java.util.List;
import java.util.Timer;
import java.util.UUID;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import no.ffi.gismoidm.attrex.BooleanOperand;
import no.ffi.gismoidm.attrex.Parser;
import no.ffi.gismoidm.discovery.DiscoveryAnnouncement;
import no.ffi.gismoidm.discovery.DiscoveryRequest;
import no.ffi.gismoidm.idp.Config;
import no.ffi.gismoidm.invocations.ServiceInvocation;
import no.ffi.gismoidm.invocations.ServiceInvocationFactory;
import no.ffi.gismoidm.utils.ClientProxy;
import no.ffi.gismoidm.utils.CredentialsNotAcceptedException;
import no.ffi.gismoidm.utils.IdentityStatement;
import no.ffi.gismoidm.utils.GismoIdmException;
import no.ffi.tpm.crypto.CryptoOperations;
import no.ffi.tpm.crypto.CryptoOperationsFactory;

/**
 *
 * @author anders
 */
public abstract class ServiceContainer extends javax.servlet.http.HttpServlet {
    private BooleanOperand accessRequirements;
    private List serviceDescription;
    private CryptoOperations myCrOp;
    private IdentityStatement myIdentityStatement;
    private long issuingClockCounter;
    private int requiredAuthenticationProtocol;
    private boolean serverIsActive = true;
    private boolean acceptsExpiredIS = false;

    public final void init() {
        // Read Config file (
        Config.loadPropertiesFrom(null);
        String crEng = Config.getProperty("CRYPTO_ENGINE", "JCE"); // or "TPM"
        myCrOp = CryptoOperationsFactory.getInstance(crEng,getServicePrefix()); 
        // Call container initializer
        initContainer();
        
        // Find access requirements;
        String ar = this.getAccessRequirements();
        try {
            accessRequirements = Parser.parse(getAccessRequirements());
        } catch (Exception e) { e.printStackTrace(); }
        
        // Find service description
        serviceDescription = this.getServiceDescription();
        
        // Does this service accept expired IS?
        acceptsExpiredIS = acceptsExpiredIS();
        

        // Determine the required authentication protocol
        try {
            String authP = Config.getProperty(getServicePrefix() + "_PROTOCOL_CLASS", "STATEFUL");
            if (authP.equalsIgnoreCase("STATEFUL"))
                requiredAuthenticationProtocol = ServiceInvocationFactory.STATEFUL;
            else if (authP.equalsIgnoreCase("STATELESS"))
                requiredAuthenticationProtocol = ServiceInvocationFactory.STATELESS;
            else
                throw new Exception("Illegal value for authentication protocol");
        } catch (Exception e) { e.printStackTrace(); }
        
        // If this appserverhost should announce the services,
        // they must be initialized at this moment, not on-demand
        String serviceDirectoryURL = Config.getProperty("DIRECTORY_SERVICE_URL");
        if (serviceDirectoryURL != null && serviceDescription != null) {
            // Send info about the service components to the service directory
            System.out.println("Starting announce thread");
            announceServices();
        }
    }
    
    protected final void doPost(HttpServletRequest request, HttpServletResponse response)
    throws ServletException, IOException {
        ObjectInputStream ois = new ObjectInputStream(request.getInputStream());
        ObjectOutputStream oos = new ObjectOutputStream(response.getOutputStream());
        response.setContentType("application/x-javaobjects");
        try {
            Object o = ois.readObject();
            System.out.println("Invocation object read");
            System.out.println("Class is " + o.getClass().getName());
            if (o instanceof ServiceInvocation) {
                ServiceInvocation sio = (ServiceInvocation)o;
                ServiceInvocation sRes = processRequest(sio);
                // Return the invocation object
                oos.writeObject(sRes);
            }
        } catch (Exception e) {oos.writeObject(e); } // Return the exception as return value
    }
            
        
    protected final void doGet(HttpServletRequest request, HttpServletResponse response)
    throws ServletException, IOException {
        response.setContentType("text/html;charset=UTF-8");
        PrintWriter out = response.getWriter();
        try {
            out.println("<html>");
            out.println("<head>");
            out.println("<title>Servlet ServiceContainer</title>");
            out.println("</head>");
            out.println("<body>");
            out.println("<h3>Servlet ServiceContainer at " + request.getContextPath () + "</h3>");
            out.println("</body>");
            out.println("</html>");
        } finally {
            out.close();
        }
    } 
       
    public ServiceInvocation processRequest(ServiceInvocation sio) throws Exception {
        System.out.println("Parameter: " + sio.getParameters());
        System.out.println("NONCE: " + sio.getNonce());
        
        // Renew the identityStatement if it less than 10 minutes left to expiration
        // To avoid that several threads request ISes at the same time, this block need to be synchronized
        synchronized (this) {
            if (myIdentityStatement == null || myIdentityStatement.identityStatementExpired(10)) {  // TODO replace 10 with variable
                try {
                    myIdentityStatement = renewIdentityStatement();
                } catch (Exception e) {
                    System.out.println("Unable to renew IS. Will keep using the old one");
                }
                issuingClockCounter =  myIdentityStatement.getIssuingTimestamp() - System.currentTimeMillis();
                System.out.println("System side issuing clock counter is " + issuingClockCounter);
            }
        }

        // Check that proposed authentication protocol is acceptable
        int authP = sio.getAuthenticationProtocol();
        if (authP != requiredAuthenticationProtocol)
            throw new CredentialsNotAcceptedException("Unrecognized authenticationProtocol");
        
        PublicKey trustAnchor = myCrOp.getTrustAnchor();
        sio.validateRequest(trustAnchor,issuingClockCounter+System.currentTimeMillis(),acceptsExpiredIS);
        System.out.println("Credentials are validated");

        // Create a ClientProperties object
        IdentityStatement clientIdentityStatement = sio.getIdentityStatement();
        System.out.println("Subject IS: " + clientIdentityStatement);
        SubjectProperties clProps = new SubjectProperties(clientIdentityStatement);

        Serializable po = sio.getParameters(); // Parameter object
        UUID nonce = sio.getNonce();
        System.out.println("Parameters are:"+po);
     
        // Check if there are required attributes for this class, to restrict its access
        System.out.println("Checking access restrictions");
        if (!accessRequirements.getBooleanValue(clProps.getAttributes()).truthValue())
            throw new GismoIdmException("Client does not have the required roles for access");
        
        // The client will check if the service meets the client's requirements
                
        // Now find and call the service method with two parameters:
        // the parameter object, and the subject properties
        // First check if parameter object is of the right class
        Serializable returnValue;
        Class par = getParameterClass();
        if (par.isInstance(po)) 
            returnValue = executeService(po,clProps); 
        else 
            returnValue = new GismoIdmException("Parameter object is wrong class:" + po.getClass().getName());

        // Make a return object
        ServiceInvocationFactory sif = new ServiceInvocationFactory(requiredAuthenticationProtocol);
        ServiceInvocation sRes = sif.createResponseInstance
                (returnValue, clientIdentityStatement, myIdentityStatement, nonce, myCrOp);
        return sRes;

    }
    
    private IdentityStatement renewIdentityStatement() throws Exception {
        // Retrieve server's identity statement (identityStatement)
//        X509Certificate myCertificate = myCrOp.certifyPublicKey();
//        if (myCertificate == null) 
//            throw new POJOIdmException("The Appserver's certificate does not seem "+
//                    "to have been correctly loaded. Please check the configuration.");
//        
//        String dn = myCertificate.getSubjectX500Principal().getName();
        Serializable isRequest = myCrOp.getISrequest();
        System.out.println("Request IS for " + myCrOp.getSubjectPrincipal().getName());
        String idpURL = Config.getProperty("LOCAL_IDP_URL");
        IdentityStatement is = ClientProxy.getIdentityStatement(isRequest,idpURL,myCrOp);
        is.validateIdentityStatement(myCrOp.getTrustAnchor());
        System.out.println("IdentityStatement renewed:" + is);
        return is;
    }
    
    @Override
    public void destroy() {
        System.out.append("ServiceContainer.destroy: We are being taken out of service");
        serverIsActive = false;
    }
    
    private void announceServices() {
        Announcer ann = new Announcer();
        ann.setDaemon(true);
        ann.start();
    }
    
    class Announcer extends Thread {
        public void run() {
            try {
                ClientProxy cp = new ClientProxy(myCrOp); 
                while (serverIsActive) {
                    DiscoveryAnnouncement da = new DiscoveryAnnouncement();
                    da.serviceURL = Config.getProperty(getServicePrefix() + "_SERVICE_URL");
                    da.idpURL = Config.getProperty("LOCAL_IDP_URL");
                    da.setParameterClass(getParameterClass());
                    da.protocolClass = requiredAuthenticationProtocol;
                    da.accessRequirements = accessRequirements;
                    da.serviceDescriptor = serviceDescription;
                    DiscoveryRequest dr = new DiscoveryRequest(da);
                    // Send the announcement object to the Discovery Service  
                    String accessReqs = "exists($DISCOVERY_PROVIDER)";
                    Serializable response = cp.invokeApplicationService("DIRECTORY", dr, accessReqs);
                    while (!cp.areCredentialsExpired(getServicePrefix())) Thread.sleep(60000); // Poll IS expiration every minute
                }
            } catch (Exception e) { e.printStackTrace(); }
        }
    }

        
    
    abstract protected void initContainer();
    protected String getAccessRequirements() {
        return Config.getProperty(getServicePrefix() + "_ACCESS_REQUIREMENTS");
    }
    protected boolean acceptsExpiredIS() {
        return Config.getProperty(getServicePrefix() + "_ACCEPTS_EXPIRED_IS") != null;
    }
    protected List getServiceDescription() { return null; } // Null means don't announce
    abstract protected String getServicePrefix();
    abstract protected Class getParameterClass();
    abstract protected Serializable executeService(Serializable parameters, SubjectProperties props);
}
